﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace WeDonekRpc.Helper.UAParser
{
    /// <summary>
    /// Represents a parser of a user agent string
    /// </summary>
    public sealed class Parser
    {
        private static readonly MinimalYamlParser _DefParser;

        static Parser ()
        {
            string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "regexes.yaml");
            string text = File.ReadAllText(path, Encoding.UTF8);
            _DefParser = new MinimalYamlParser(text);
        }
        /// <summary>
        /// The constant string value used to signal an unknown match for a given property or value. This
        /// is by default the string "Other".
        /// </summary>
        public const string Other = "Other";

        private readonly Func<string, OS> _osParser;
        private readonly Func<string, Device> _deviceParser;
        private readonly Func<string, UserAgent> _userAgentParser;

        private Parser ( MinimalYamlParser yamlParser, ParserOptions options )
        {
            Config config = new Config(options ?? new ParserOptions());

            this._userAgentParser = CreateParser(Read(yamlParser.ReadMapping("user_agent_parsers"), config.UserAgentSelector), new UserAgent(Other, null, null, null));
            this._osParser = CreateParser(Read(yamlParser.ReadMapping("os_parsers"), config.OSSelector), new OS(Other, null, null, null, null));
            this._deviceParser = CreateParser(Read(yamlParser.ReadMapping("device_parsers"), config.DeviceSelector), new Device(Other, string.Empty, string.Empty));
        }

        private static IEnumerable<T> Read<T> ( IEnumerable<Dictionary<string, string>> entries, Func<Func<string, string>, T> selector )
        {
            return from cm in entries select selector(cm.Find);
        }

        /// <summary>
        /// Returns a <see cref="Parser"/> instance based on the regex definitions in a yaml string
        /// </summary>
        /// <param name="yaml">a string containing yaml definitions of reg-ex</param>
        /// <param name="parserOptions">specifies the options for the parser</param>
        /// <returns>A <see cref="Parser"/> instance parsing user agent strings based on the regexes defined in the yaml string</returns>
        public static Parser FromYaml ( string yaml, ParserOptions parserOptions = null )
        {
            return new Parser(new MinimalYamlParser(yaml), parserOptions);
        }

        /// <summary>
        /// Returns a <see cref="Parser"/> instance based on the embedded regex definitions.
        /// <remarks>The embedded regex definitions may be outdated. Consider passing in external yaml definitions using <see cref="Parser.FromYaml"/></remarks>
        /// </summary>
        /// <param name="parserOptions">specifies the options for the parser</param>
        /// <returns></returns>
        public static Parser GetDefault ( ParserOptions parserOptions = null )
        {
            return new Parser(_DefParser, parserOptions);
        }

        /// <summary>
        /// Parse a user agent string and obtain all client information
        /// </summary>
        public ClientInfo Parse ( string uaString )
        {
            OS os = this.ParseOS(uaString);
            Device device = this.ParseDevice(uaString);
            UserAgent ua = this.ParseUserAgent(uaString);
            return new ClientInfo(uaString, os, device, ua);
        }

        /// <summary>
        /// Parse a user agent string and obtain the OS information
        /// </summary>
        public OS ParseOS ( string uaString ) { return this._osParser(uaString); }
        /// <summary>
        /// Parse a user agent string and obtain the device information
        /// </summary>
        public Device ParseDevice ( string uaString ) { return this._deviceParser(uaString); }
        /// <summary>
        /// Parse a user agent string and obtain the UserAgent information
        /// </summary>
        public UserAgent ParseUserAgent ( string uaString ) { return this._userAgentParser(uaString); }

        private static Func<string, T> CreateParser<T> ( IEnumerable<Func<string, T>> parsers, T defaultValue ) where T : class
        {
            return CreateParser(parsers, defaultValue, t => t);
        }

        private static Func<string, TResult> CreateParser<T, TResult> ( IEnumerable<Func<string, T>> parsers, T defaultValue, Func<T, TResult> selector ) where T : class
        {
            parsers = parsers?.ToArray() ?? Enumerable.Empty<Func<string, T>>();
            return ua => selector(parsers.Select(p => p(ua)).FirstOrDefault(m => m != null) ?? defaultValue);
        }

        private class Config
        {
            private readonly ParserOptions _options;

            internal Config ( ParserOptions options )
            {
                this._options = options;
            }

            // ReSharper disable once InconsistentNaming
            public Func<string, OS> OSSelector ( Func<string, string> indexer )
            {
                Regex regex = this.Regex(indexer, "OS");
                string os = indexer("os_replacement");
                string v1 = indexer("os_v1_replacement");
                string v2 = indexer("os_v2_replacement");
                string v3 = indexer("os_v3_replacement");
                string v4 = indexer("os_v4_replacement");
                return Parsers.OS(regex, os, v1, v2, v3, v4);
            }

            public Func<string, UserAgent> UserAgentSelector ( Func<string, string> indexer )
            {
                Regex regex = this.Regex(indexer, "User agent");
                string family = indexer("family_replacement");
                string v1 = indexer("v1_replacement");
                string v2 = indexer("v2_replacement");
                string v3 = indexer("v3_replacement");
                return Parsers.UserAgent(regex, family, v1, v2, v3);
            }

            public Func<string, Device> DeviceSelector ( Func<string, string> indexer )
            {
                Regex regex = this.Regex(indexer, "Device", indexer("regex_flag"));
                string device = indexer("device_replacement");
                string brand = indexer("brand_replacement");
                string model = indexer("model_replacement");
                return Parsers.Device(regex, device, brand, model);
            }

            private Regex Regex ( Func<string, string> indexer, string key, string regexFlag = null )
            {
                string pattern = indexer("regex");
                if ( pattern == null )
                {
                    throw new Exception($"{key} is missing regular expression specification.");
                }

                // Some expressions in the regex.yaml file causes parsing errors
                // in .NET such as the \_ token so need to alter them before
                // proceeding.

                if ( pattern.IndexOf(@"\_", StringComparison.Ordinal) >= 0 )
                {
                    pattern = pattern.Replace(@"\_", "_");
                }

                //Singleline: User agent strings do not contain newline characters. RegexOptions.Singleline improves performance.
                //CultureInvariant: The interpretation of a user agent never depends on the current locale.
                RegexOptions options = RegexOptions.Singleline | RegexOptions.CultureInvariant;

                if ( "i".Equals(regexFlag) )
                {
                    options |= RegexOptions.IgnoreCase;
                }

#if REGEX_COMPILATION
                if (_options.UseCompiledRegex)
                {
                    options |= RegexOptions.Compiled;
                }
#endif

#if REGEX_MATCHTIMEOUT

                return new Regex(pattern, options, _options.MatchTimeOut);
#else
                return new Regex(pattern, options);
#endif
            }
        }
    }
}
